#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <getopt.h>
#include <unistd.h>
#include <signal.h>

#include "../util.h"
#include "../getopt_helper.h"

#include "linked_list.h"

#define FILEMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)

static struct option const LONG_OPTIONS[] = {
	{ "append", no_argument, NULL, 'a' },
	{ "ignore-interrupts", no_argument, NULL, 'i' },
	{ "output-error", optional_argument, NULL, 'p' },

	GETOPT_END_DECL
};

static const char usage_str[] =
    "Usage: %s [OPTION]... [FILE]...\n"
    "Copy standard input to each FILE, and also to standard output.\n"
    "\n"
    "  -a, --append              append to the given FILEs, do not overwrite\n"
    "  -i, --ignore-interrupts   ignore interrupt signals\n"
    "  -p                        diagnose errors writing to non pipes\n"
    "      --output-error[=MODE] set behavior on write error.  See MODE "
    "below\n"
    "      --help                display this help and exit\n"
    "      --version             output version information and exit\n"
    "\n"
    "MODE determines behavior with write errors on the outputs:\n"
    "  'warn'         diagnose errors writing to any output\n"
    "  'warn-nopipe'  diagnose errors writing to any output not a pipe\n"
    "  'exit'         exit on error writing to any output\n"
    "  'exit-nopipe'  exit on error writing to any output not a pipe\n"
    "The default MODE for the -p option is 'warn-nopipe'.\n"
    "The default operation when --output-error is not specified, is to\n"
    "exit immediately on error writing to a pipe, and diagnose errors\n"
    "writing to non pipe outputs.";

enum output_error {
	OUTPUT_ERROR_SIGPIPE,	  /* traditional behavior, sigpipe enabled.  */
	OUTPUT_ERROR_WARN,	  /* warn on EPIPE, but continue.  */
	OUTPUT_ERROR_WARN_NOPIPE, /* ignore EPIPE, continue.  */
	OUTPUT_ERROR_EXIT,	  /* exit on any output error.  */
	OUTPUT_ERROR_EXIT_NOPIPE, /* exit on any output error except EPIPE.  */
} output_error = OUTPUT_ERROR_SIGPIPE;

LL_DEF(node, int fd, char *name);

static inline void error(const char *fmt, ...);
static void	   list_prepend(node **head, int fd, char *name);

int
main(int argc, char *argv[])
{
	int ch, ret = 0;

	enum flag INIT_FLAGS(NO, a, i);

	while ((ch = getopt_long(argc, argv, "aip", LONG_OPTIONS, NULL))
	       != -1) {
		switch (ch) {
			IS_FLAGS(YES, a, i);

		case 'p':
			if (!optarg) {
				output_error = OUTPUT_ERROR_WARN_NOPIPE;
				break;
			}

			if (STREQ(optarg, "warn"))
				output_error = OUTPUT_ERROR_WARN;
			else if (STREQ(optarg, "warn-nopipe"))
				output_error = OUTPUT_ERROR_WARN_NOPIPE;
			else if (STREQ(optarg, "exit"))
				output_error = OUTPUT_ERROR_EXIT;
			else if (STREQ(optarg, "exit-nopipe"))
				output_error = OUTPUT_ERROR_EXIT_NOPIPE;
			else
				errx(!0,
				     "invalid argument for --output-error: %s",
				     optarg);
			break;

			GETOPT_END_CASE
		}
	}

	if (iflag) signal(SIGINT, SIG_IGN);

	if (output_error != OUTPUT_ERROR_SIGPIPE) signal(SIGPIPE, SIG_IGN);

	node *list = NULL;

	for (argv += optind; *argv; ++argv) {
		int fd;
		if ((fd = open(*argv,
			       O_WRONLY | O_CREAT
				   | (aflag ? O_APPEND : O_TRUNC),
			       FILEMODE))
		    == -1) {
			error("%s", *argv);
			ret = !0;
		} else {
			list_prepend(&list, fd, *argv);
		}
	}
	list_prepend(&list, STDOUT_FILENO, "stdout");

	node   *i;
	char	buf[BUFSIZ];
	ssize_t readed;

	const enum flag fail = output_error == OUTPUT_ERROR_EXIT
			    || output_error == OUTPUT_ERROR_WARN;
	while ((readed = read(STDIN_FILENO, buf, sizeof(buf)))) {
		if (readed < 0 && errno == EINTR) continue;
		if (readed <= 0) break;

		LL_FOREACH(list, i)
		{
			char  *ptr = buf;
			size_t n, writed;
			SAFE_ASSIGN(n, readed);
			do {
				if ((ssize_t)SAFE_ASSIGN(writed,
							 write(i->fd, ptr, n))
				    < 0) {
					if (errno != EPIPE || fail) {
						error("%s", i->name);
						ret = !0;
					}
					break;
				}
				ptr += writed;
			} while ((n -= writed));
		}
	}
	if (readed == -1) {
		warn("read");
		ret = !0;
	}

	LL_FOREACH(list, i)
	{
		if (close(i->fd) == -1) {
			warn("%s", i->name);
			ret = !0;
		}
		LL_DELETE(list, i);
		free(i);
	}

	return ret;
}

static inline void
error(const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);

	if (output_error == OUTPUT_ERROR_EXIT
	    || output_error == OUTPUT_ERROR_EXIT_NOPIPE)
		verr(!0, fmt, ap);
	else
		vwarn(fmt, ap);

	va_end(ap);
}

static void
list_prepend(node **head, int fd, char *name)
{
	node *item;

	if (!(item = calloc(1, sizeof(*item)))) NO_MEMORY();

	item->fd   = fd;
	item->name = name;

	LL_PREPEND(*head, item);
}
